(ns io.github.jmjoy.container
  (:import [clojure.lang Atom]
           [io.github.jmjoy.container.exception ContainerException]))

(declare resolve-instance)
(declare bind-instance)

(defprotocol ContainerProtocol
  "Container Protocol."
  (bind [this abstract concrete] "Register a binding with the container.")
  (singleton [this abstract concrete] "Register a shared binding in the container.")
  (has [this abstract] "Determine if the container has a binding.")
  (make [this abstract] "Resolve the given type from the container."))

(defrecord Container [^Atom bindings ^Atom instances]
  ;; Implement ContainerProtocol
  ContainerProtocol

  (bind [this abstract concrete]
    (bind-instance this abstract concrete false))

  (singleton [this abstract concrete]
    (bind-instance this abstract concrete true))

  (make [this abstract]
    (if (contains? @instances abstract)
      (get @instances abstract)
      (resolve-instance this abstract)))

  (has [this abstract]
    (contains? @bindings abstract)))

(defn- resolve-instance
  "Resolve instance not share."
  [^Container container abstract]
  (when-not (.has container abstract)
    (throw (ContainerException. "Resolve error: No binding for abstract")))
  (let [{:keys [concrete share]} (get @(.bindings container) abstract)]
    (let [instance (concrete)]
      (when share
        (swap! (.instances container) assoc abstract instance))
      instance)))

(defn- bind-instance
  "Bind instance share or not."
  [^Container container abstract concrete share]
  (swap! (.bindings container) assoc abstract {:concrete concrete :share share}))

(defn NewContainer
  "Get a new Container instance."
  []
  (Container. (atom {}) (atom {})))

(def global-container (NewContainer))

(defn bind
  "Register a binding with the container."
  [abstract concrete]
  (.bind global-container abstract concrete))

(defn singleton
  "Register a shared binding in the container."
  [abstract concrete]
  (.singleton global-container abstract concrete))

(defn has
  "Determine if the container has a binding."
  [abstract]
  (.has global-container abstract))

(defn make
  "Resolve the given type from the container."
  [abstract]
  (.make global-container abstract))

